package org.apache.lucene.ocean;

import java.io.IOException;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.ocean.DirectoryIndex.DirectoryIndexSnapshot;
import org.apache.lucene.ocean.DiskIndex.DiskIndexSnapshot;
import org.apache.lucene.ocean.Index.IndexSnapshot;
import org.apache.lucene.ocean.Index.IndexSnapshotSearchable;
import org.apache.lucene.ocean.RamIndex.RamIndexSnapshot;
import org.apache.lucene.ocean.SnapshotInfo.IndexInfo;
import org.apache.lucene.ocean.WriteableMemoryIndex.MemoryIndexSnapshot;
import org.apache.lucene.ocean.util.SortedListMap;
import org.apache.lucene.search.Searcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** 
 * Snapshot in the Ocean system is a set of IndexSnapshot classes.  Each IndexSnapshot is of 
 * an Index.
 */
public class Snapshot implements Comparable<Snapshot> {
  final static Logger LOG = LoggerFactory.getLogger(Snapshot.class);
  /**
   * Transaction ID
   */
  private BigDecimal id;
  /**
   * IndexID to IndexSnapshot sortedListMap
   */
  private SortedListMap<IndexID,IndexSnapshot> indexSnapshotMap;
  /**
   * The writeable indexsnapshot
   */
  private MemoryIndexSnapshot writeableSnapshot;
  /**
   * IndexReader for this Snapshot
   */
  private IndexReader indexReader;
  private IndexSnapshotSearchable[] searchables;
  /**
   * Starts
   */
  private final int[] starts;
  /**
   * Max doc for this snapshot
   */
  private int maxDoc;
  private TransactionSystem system;
  private final long timestamp;
  /**
   * Number of references to the snapshot
   */
  private int refs = 0;
  
  /**
   * 
   * @param id Transaction ID
   * @param writeableSnapshot Writeable IndexSnapshot
   * @param indexSnapshots IndexSnapshots
   * @param system 
   * @param timestamp Date when the snapshot was created
   * @throws IOException
   */
  public Snapshot(BigDecimal id, MemoryIndexSnapshot writeableSnapshot, Collection<IndexSnapshot> indexSnapshots, TransactionSystem system,
      long timestamp) throws IOException {
    this.id = id;
    this.writeableSnapshot = writeableSnapshot;
    this.timestamp = timestamp;
    indexSnapshotMap = new SortedListMap<IndexID,IndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      assert indexSnapshot != null;
      indexSnapshotMap.put(indexSnapshot.getIndex().getId(), indexSnapshot);
    }
    indexSnapshotMap.put(writeableSnapshot.getIndex().getId(), writeableSnapshot);
    IndexReader[] readerArray = getReaderArray(indexSnapshotMap.values());
    int i = 0;
    //searchables = new IndexSnapshotSearchable[indexSnapshotMap.size()];
    List<IndexSnapshotSearchable> searchablesList = new ArrayList<IndexSnapshotSearchable>(indexSnapshotMap.size());
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      IndexSnapshotSearchable searcher = indexSnapshot.getSearcher();
      if (searcher == null) {
        LOG.error("searcher null");
      }
      if (searcher != null) searchablesList.add(searcher);
    }
    searchables = (IndexSnapshotSearchable[])searchablesList.toArray(new IndexSnapshotSearchable[0]);
    indexReader = new MultiReader(readerArray);
    maxDoc = indexReader.maxDoc();
    starts = makeStarts();
  }
  
  public Snapshot(Long snapshotId, int minorVersion, MemoryIndexSnapshot writeableSnapshot, List<IndexSnapshot> indexSnapshots,
      TransactionSystem system, long timestamp) throws IOException {
    this(toId(snapshotId, minorVersion), writeableSnapshot, indexSnapshots, system, timestamp);
  }
  
  public Searcher getSearcher() throws IOException {
    return new OceanSearcher(this);
  }
  
  public String getIdString() {
    return formatId(id);
  }
  
  public IndexSnapshotSearchable[] getSearchables() {
    return searchables;
  }
  
  public boolean isMinor() {
    return getMinorVersion() > 0;
  }
  
  public String toString() {
    return "snapshot: "+formatId(id);
  }
  
  public TransactionSystem getSystem() {
    return system;
  }
  
  public synchronized int refCount() {
    return refs;
  }
  
  public synchronized boolean hasRefs() {
    return refs > 0;
  }
  
  public synchronized void decRef() {
    refs--;
  }
  
  synchronized void incRef() {
    refs++;
  }
  
  private static boolean hasDuplicates(List<IndexSnapshot> allIndexSnapshots) {
    Set<IndexID> set = new HashSet<IndexID>();
    for (IndexSnapshot indexSnapshot : allIndexSnapshots) {
      IndexID indexId = indexSnapshot.getIndex().getId();
      if (set.contains(indexId)) {
        return true;
      } else {
        set.add(indexId);
      }
    }
    return false;
  }
  
  private IndexReader[] getReaderArray(Collection<IndexSnapshot> indexSnapshots) {
    IndexReader[] readerArray = new IndexReader[indexSnapshots.size()];
    int x = 0;
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      indexSnapshotMap.put(indexSnapshot.getIndex().getId(), indexSnapshot);
      readerArray[x] = indexSnapshot.getIndexReader();
      x++;
    }
    return readerArray;
  }
  
  public int numDocs() {
    return indexReader.numDocs();
  }
  
  private int[] makeStarts() {
    IndexSnapshot[] indexSnapshotsArray = indexSnapshotMap.values().toArray(new IndexSnapshot[0]);
    // build starts array
    int[] starts = new int[indexSnapshotsArray.length + 1];
    for (int i = 0; i < indexSnapshotsArray.length; i++) {
      starts[i] = maxDoc;
      maxDoc += indexSnapshotsArray[i].maxDoc(); // compute maxDocs
    }
    starts[indexSnapshotsArray.length] = maxDoc;
    return starts;
  }
  
  public long getTimestamp() {
    return timestamp;
  }

  public int compareTo(Snapshot other) {
    return id.compareTo(other.id);
  }

  //public Searcher getSearcher() throws IOException {
  //  MultiSearcher multiSearcher = new MultiSearcher(getSearchers());
  //  return multiSearcher;
  //}
  
  
  /**
  public Searcher[] getSearchers() {
    IndexSnapshot[] indexSnapshots = (IndexSnapshot[]) indexSnapshotMap.values().toArray(new IndexSnapshot[0]);
    Searcher[] searchers = new Searcher[indexSnapshotMap.size()];
    for (int x = 0; x < indexSnapshots.length; x++) {
      searchers[x] = new SnapshotSearcher(indexSnapshots[x].getIndexReader(), indexSnapshots[x]);
    }
    return searchers;
  }
  **/
  public IndexReader getIndexReader() {
    return indexReader;
  }

  public int maxDoc() {
    return maxDoc;
  }

  public int[] getStarts() {
    return starts;
  }

  public List<RamIndexSnapshot> getRamIndexSnapshots() {
    List<RamIndexSnapshot> ramIndexSnapshots = new ArrayList<RamIndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      if (indexSnapshot instanceof RamIndexSnapshot) {
        ramIndexSnapshots.add((RamIndexSnapshot) indexSnapshot);
      }
    }
    return ramIndexSnapshots;
  }

  public IndexReader[] getIndexReaders() {
    IndexReader[] indexReaders = new IndexReader[indexSnapshotMap.size()];
    int i = 0;
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      indexReaders[i] = indexSnapshot.getIndexReader();
      i++;
    }
    return indexReaders;
  }

  public int getMaxDoc() {
    int maxDoc = 0;
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      maxDoc += indexSnapshot.getIndexReader().maxDoc();
    }
    return maxDoc;
  }

  public int getMinorVersion() {
    return getMinorVersion(id);
  }

  public static void main(String[] args) {
    BigDecimal id = toId(210l, 1);
    String string = formatId(id);
    System.out.println(string);
  }

  public static BigDecimal toId(Long snapshotId, int minorVersion) {
    StringBuilder builder = new StringBuilder();
    builder.append(snapshotId);
    builder.append(".");
    if (10 > minorVersion)
      builder.append("0");
    builder.append(minorVersion);
    BigDecimal value = new BigDecimal(builder.toString());
    return value;
  }

  public static int getMinorVersion(BigDecimal value) {
    value = value.subtract(new BigDecimal(value.longValue()));
    BigDecimal decimal = value.scaleByPowerOfTen(2);
    return decimal.intValue();
  }
  
  public static List<Long> getSnapshotIds(Collection<IndexSnapshot> indexSnapshots) {
    List<Long> ids = new ArrayList<Long>();
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      ids.add(indexSnapshot.getSnapshotId());
    }
    return ids;
  }
  /**
  public static boolean snapshotIdsMatch(Collection<IndexSnapshot> indexSnapshots) {
    Long current = null;
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      if (current == null) {
        current = indexSnapshot.getSnapshotId();
      } else if (!current.equals(indexSnapshot.getSnapshotId())) {
        return false;
      }
    }
    return true;
  }
  **/
  public SnapshotInfo getSnapshotInfo() throws IOException {
    int deletedDocs = indexReader.maxDoc() - indexReader.numDocs();
    SnapshotInfo snapshotInfo = new SnapshotInfo(id, indexReader.maxDoc(), indexReader.numDocs(), deletedDocs, timestamp);
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      Index index = indexSnapshot.getIndex();
      String type = null;
      Long segmentGeneration = null;
      Long deleteFlushId = null;
      Long lastAppliedId = null;
      List<IndexID> mergedFrom = index.getMergedFromIndexIds();
      if (index instanceof DiskIndex) {
        segmentGeneration = indexSnapshot.getIndexReader().getIndexCommit().getGeneration();
        type = "disk";
      } else if (index instanceof WriteableMemoryIndex)
        type = "memory";
      else if (index instanceof RamIndex)
        type = "ram";
      if (indexSnapshot instanceof DirectoryIndexSnapshot) {
        DirectoryIndexSnapshot directoryIndexSnapshot = (DirectoryIndexSnapshot)indexSnapshot;
        deleteFlushId = directoryIndexSnapshot.getLastCheckpointId();
        lastAppliedId = directoryIndexSnapshot.getLastAppliedSnapshotId();
      }
      IndexInfo indexInfo = new IndexInfo(indexSnapshot.getSnapshotId(), index.getId().id, segmentGeneration, type, indexSnapshot.maxDoc(), indexSnapshot.getIndexReader().numDocs(), indexSnapshot.deletedDoc(), indexSnapshot.getMinDocumentId(), indexSnapshot.getMaxDocumentId(), indexSnapshot.getMinSnapshotId(), indexSnapshot.getMaxSnapshotId(), deleteFlushId, lastAppliedId, mergedFrom);
      snapshotInfo.add(indexInfo);
    }
    return snapshotInfo;
  }

  public static String formatId(BigDecimal id) {
    DecimalFormat format = new DecimalFormat("##0.00");
    return format.format(id);
  }

  public static String getFileName(BigDecimal id) {
    String string = formatId(id);
    String replaced = string.replace('.', '_');
    return "snapshot_" + replaced + ".xml";
  }

  /**
   * Create minor snapshot (meaning a merged snapshot with no real index
   * changes) reusing the existing writeableSnapshot.
   * 
   * @param removeIndexIds
   * @param newIndexSnapshot
   * @return
   * @throws IOException
   */
  public Snapshot createMinor(Collection<IndexID> removeIndexIds, IndexSnapshot newIndexSnapshot) throws IOException {
    return createMinor(removeIndexIds, writeableSnapshot, newIndexSnapshot);
  }
  
  public Snapshot createMinor(Collection<IndexID> removeIndexIds) throws IOException {
    int minorVersion = getMinorVersion();
    Long snapshotId = getSnapshotId();
    int newMinorVersion = minorVersion + 1;
    HashMap<IndexID,IndexSnapshot> mapCopy = new HashMap<IndexID,IndexSnapshot>(indexSnapshotMap);
    for (IndexID indexid : removeIndexIds) {
      IndexSnapshot removed = mapCopy.remove(indexid);
      if (removed != null) {
        System.out.println("removed indexid: "+indexid+" from snapshot: "+snapshotId+"."+newMinorVersion);
      }
    }
    // make sure to put in the writeableSnapshot
    mapCopy.put(writeableSnapshot.getIndex().getId(), writeableSnapshot);
    System.out.println("mapCopy.values(): "+mapCopy.values());
    Snapshot newSnapshot = new Snapshot(snapshotId, newMinorVersion, writeableSnapshot, new ArrayList(mapCopy.values()), system, System
        .currentTimeMillis());
    return newSnapshot;
  }

  public Snapshot createMinor(Collection<IndexID> removeIndexIds, MemoryIndexSnapshot writeableSnapshot, IndexSnapshot newIndexSnapshot)
      throws IOException {
    int minorVersion = getMinorVersion();
    Long snapshotId = getSnapshotId();
    int newMinorVersion = minorVersion + 1;
    HashMap<IndexID,IndexSnapshot> mapCopy = new HashMap<IndexID,IndexSnapshot>(indexSnapshotMap);
    for (IndexID indexid : removeIndexIds) {
      IndexSnapshot removed = mapCopy.remove(indexid);
      if (removed != null) {
        LOG.info("removed indexid: "+indexid+" from snapshot: "+snapshotId+"."+newMinorVersion);
      }
    }
    IndexID newIndexId = newIndexSnapshot.getIndex().getId();
    assert !mapCopy.containsKey(newIndexId);
    mapCopy.put(newIndexId, newIndexSnapshot);
    mapCopy.put(writeableSnapshot.getIndex().getId(), writeableSnapshot);
    
    LOG.info("snapshotId: " + snapshotId + " newMinorVersion: " + newMinorVersion+" newIndexId: "+newIndexId);
    // BigDecimal newId = toId(snapshotId, minorVersion);
    Snapshot newSnapshot = new Snapshot(snapshotId, newMinorVersion, writeableSnapshot, new ArrayList(mapCopy.values()), system, System
        .currentTimeMillis());
    return newSnapshot;
  }

  public List<DiskIndex> getDiskIndexes() {
    List<DiskIndex> diskIndexes = new ArrayList<DiskIndex>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      if (indexSnapshot instanceof DiskIndexSnapshot) {
        diskIndexes.add((DiskIndex)indexSnapshot.getIndex());
      }
    }
    return diskIndexes;
  }

  public List<Index> getDeleteOnlyIndices() {
    HashMap<IndexID,IndexSnapshot> mapCopy = new HashMap<IndexID,IndexSnapshot>(indexSnapshotMap);
    mapCopy.remove(writeableSnapshot.getIndex().getId());
    List<Index> indices = new ArrayList<Index>();
    for (IndexSnapshot indexSnapshot : mapCopy.values()) {
      indices.add(indexSnapshot.getIndex());
    }
    return indices;
  }

  public MemoryIndexSnapshot getWriteableSnapshot() {
    return writeableSnapshot;
  }

  public boolean containsIndex(long indexid) {
    return indexSnapshotMap.containsKey(indexid);
  }
  
  public List<IndexSnapshot> getDeleteOnlyIndexSnapshots() {
    List<IndexSnapshot> indexSnapshots = new ArrayList<IndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      if (indexSnapshot != writeableSnapshot) {
        indexSnapshots.add(indexSnapshot);
      }
    }
    return indexSnapshots;
  }
  
  public List<DiskIndexSnapshot> getDiskIndexSnapshots() {
    List<DiskIndexSnapshot> diskIndexSnapshots = new ArrayList<DiskIndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      if (indexSnapshot instanceof DiskIndexSnapshot) {
        diskIndexSnapshots.add((DiskIndexSnapshot) indexSnapshot);
      }
    }
    return diskIndexSnapshots;
  }

  public List<IndexSnapshot> getIndexSnapshots() {
    return new ArrayList(indexSnapshotMap.values());
  }

  public Long getSnapshotId() {
    return id.longValue();
  }

  public BigDecimal getId() {
    return id;
  }
}
